CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/share/public_paths/page/[page].tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
This is simply a list of *all* publicly shared files/directories,
8
with a simple page. It is mainly meant to be walked by crawlers
9
such as Google and for people to browse.
10
*/
11
12
import { useEffect, useState } from "react";
13
import { Alert, Button, Input, Popconfirm, Radio, Space } from "antd";
14
import Link from "next/link";
15
import SiteName from "components/share/site-name";
16
import getPool, { timeInSeconds } from "@cocalc/database/pool";
17
import PublicPaths from "components/share/public-paths";
18
import { Layout } from "components/share/layout";
19
import withCustomize from "lib/with-customize";
20
import { Customize } from "lib/share/customize";
21
import GoogleSearch from "components/share/google-search";
22
import ProxyInput from "components/share/proxy-input";
23
import getAccountId from "lib/account/get-account";
24
import A from "components/misc/A";
25
import { useRouter } from "next/router";
26
import useProfile from "lib/hooks/profile";
27
import apiPost from "lib/api/post";
28
29
const PAGE_SIZE = 100;
30
31
function getPage(obj): number {
32
let { page } = obj ?? {};
33
if (page == null) {
34
return 1;
35
}
36
page = parseInt(page);
37
if (isFinite(page)) {
38
return Math.max(page, 1);
39
}
40
return 1;
41
}
42
43
function Pager({ page, publicPaths }) {
44
const router = useRouter();
45
46
return (
47
<div>
48
Page {page}
49
&nbsp;&nbsp;
50
{page > 1 ? (
51
<Link
52
href={{
53
pathname: `/share/public_paths/page/${page - 1}`,
54
query: router.query,
55
}}
56
as={`/share/public_paths/page/${page - 1}${
57
router.asPath.split("?")[1] ? "?" + router.asPath.split("?")[1] : ""
58
}`}
59
passHref
60
>
61
Previous
62
</Link>
63
) : (
64
<span style={{ color: "#888" }}>Previous</span>
65
)}
66
&nbsp;&nbsp;
67
{publicPaths != null && publicPaths.length >= PAGE_SIZE ? (
68
<Link
69
href={{
70
pathname: `/share/public_paths/page/${page + 1}`,
71
query: router.query,
72
}}
73
as={`/share/public_paths/page/${page + 1}${
74
router.asPath.split("?")[1] ? "?" + router.asPath.split("?")[1] : ""
75
}`}
76
passHref
77
>
78
Next
79
</Link>
80
) : (
81
<span style={{ color: "#888" }}>Next</span>
82
)}
83
</div>
84
);
85
}
86
87
export default function All({ page, publicPaths, customize }) {
88
const pager = <Pager page={page} publicPaths={publicPaths} />;
89
const router = useRouter();
90
const [sort, setSort] = useState<string>("last_edited");
91
92
// Set default value of `sort` from query parameter `sort`
93
useEffect(() => {
94
if (router.query.sort) {
95
setSort(router.query.sort as string);
96
}
97
}, [router.query.sort]);
98
99
function handleSortChange(e) {
100
const sort = e.target.value;
101
// Update the query parameter with new `sort` value
102
router.push({
103
pathname: router.pathname,
104
query: { ...router.query, sort },
105
});
106
}
107
108
const [search, setSearch] = useState<string>("");
109
useEffect(() => {
110
if (router.query.search) {
111
setSearch(router.query.search as string);
112
}
113
}, [router.query.search]);
114
115
function handleSearchGo(search: string) {
116
router.push({
117
pathname: router.pathname,
118
query: { ...router.query, search },
119
});
120
}
121
122
return (
123
<Customize value={customize}>
124
<Layout title={`Page ${page} of public files`}>
125
<div>
126
<Space
127
style={{
128
float: "right",
129
justifyContent: "flex-end",
130
marginTop: "7.5px",
131
}}
132
direction="vertical"
133
>
134
<GoogleSearch style={{ width: "450px", maxWidth: "90vw" }} />
135
</Space>
136
<h2>
137
Browse publicly shared documents on <SiteName />
138
</h2>
139
<ProxyInput />
140
Star items to easily <A href="/stars">find them in your list</A>
141
.
142
<br />
143
<br />
144
<Input.Search
145
allowClear
146
placeholder="Search path & description..."
147
style={{ marginLeft: "5px", float: "right", width: "275px" }}
148
value={search}
149
onChange={(e) => {
150
setSearch(e.target.value);
151
if (!e.target.value) {
152
setTimeout(() => {
153
handleSearchGo("");
154
}, 1);
155
}
156
}}
157
onSearch={() => handleSearchGo(search)}
158
onPressEnter={() => handleSearchGo(search)}
159
/>
160
<Radio.Group
161
value={sort}
162
onChange={handleSortChange}
163
style={{ float: "right" }}
164
>
165
<Radio.Button value="last_edited">Newest</Radio.Button>
166
<Radio.Button value="-last_edited">Oldest</Radio.Button>
167
<Radio.Button value="stars">Stars</Radio.Button>
168
<Radio.Button value="-stars">Least stars</Radio.Button>
169
<Radio.Button value="views">Views</Radio.Button>
170
<Radio.Button value="-views">Least views</Radio.Button>
171
</Radio.Group>
172
{pager}
173
<br />
174
{typeof router.query.search == "string" &&
175
router.query.search.trim() &&
176
publicPaths.length > 0 && (
177
<AdminUnpublish publicPaths={publicPaths} />
178
)}
179
<PublicPaths publicPaths={publicPaths} />
180
<br />
181
{pager}
182
</div>
183
</Layout>
184
</Customize>
185
);
186
}
187
188
async function adminUnpublish(id: string): Promise<void> {
189
const query = {
190
crm_public_paths: {
191
id,
192
disabled: true,
193
},
194
};
195
await apiPost("/user-query", { query });
196
}
197
198
function AdminUnpublish({ publicPaths }) {
199
const profile = useProfile();
200
const router = useRouter();
201
const [error, setError] = useState("");
202
203
if (!profile?.is_admin) return null;
204
205
const handleUnpublish = async () => {
206
setError("");
207
try {
208
await Promise.all(publicPaths.map((x) => adminUnpublish(x.id)));
209
} catch (error) {
210
setError(error.toString());
211
}
212
// refresh the current page
213
router.push({
214
pathname: router.pathname,
215
query: router.query,
216
});
217
};
218
219
return (
220
<Alert
221
style={{ margin: "0 0 15px" }}
222
type="info"
223
message={"Administrator Controls"}
224
description={
225
<div>
226
{error && (
227
<Alert
228
showIcon
229
style={{ margin: "15px 0" }}
230
message={"Error"}
231
description={error}
232
type="error"
233
closable
234
onClose={() => setError("")}
235
/>
236
)}
237
<Popconfirm
238
title={
239
<div style={{ width: "400px" }}>
240
Are you sure you want to unpublish ALL {publicPaths.length}{" "}
241
items displayed below? These items will be made completely
242
private (not visible in any way, except to collaborators).
243
</div>
244
}
245
onConfirm={handleUnpublish}
246
okText="Yes"
247
cancelText="No"
248
>
249
<Button danger>
250
Unpublish ALL {publicPaths.length} listed items...
251
</Button>
252
</Popconfirm>
253
</div>
254
}
255
/>
256
);
257
}
258
259
export async function getServerSideProps(context) {
260
const isAuthenticated = (await getAccountId(context.req)) != null;
261
const page = getPage(context.params);
262
const sort = getSort(context);
263
const { search, searchQuery } = getSearch(context);
264
const pool = getPool("medium");
265
const params = [isAuthenticated, PAGE_SIZE, PAGE_SIZE * (page - 1)];
266
if (search) {
267
params.push(search);
268
}
269
const { rows } = await pool.query(
270
`SELECT public_paths.id, public_paths.path, public_paths.url, public_paths.description, ${timeInSeconds(
271
"public_paths.last_edited",
272
"last_edited",
273
)}, projects.avatar_image_tiny,
274
counter::INT,
275
(SELECT COUNT(*)::INT FROM public_path_stars WHERE public_path_id=public_paths.id) AS stars
276
FROM public_paths, projects
277
WHERE public_paths.project_id = projects.project_id
278
AND public_paths.vhost IS NULL AND public_paths.disabled IS NOT TRUE AND public_paths.unlisted IS NOT TRUE AND
279
public_paths.url IS NULL AND
280
((public_paths.authenticated IS TRUE AND $1 IS TRUE) OR (public_paths.authenticated IS NOT TRUE))
281
${searchQuery}
282
ORDER BY ${sort} LIMIT $2 OFFSET $3`,
283
params,
284
);
285
286
return await withCustomize({ context, props: { page, publicPaths: rows } });
287
}
288
289
function getSearch(context) {
290
const { query } = context;
291
const search = query?.search || "";
292
if (search) {
293
return {
294
search: `%${search}%`,
295
searchQuery:
296
"AND (LOWER(public_paths.path) LIKE LOWER($4) OR LOWER(public_paths.description) LIKE LOWER($4))",
297
};
298
} else {
299
return { search, searchQuery: "" };
300
}
301
}
302
303
function getSort(context) {
304
switch (context.query?.sort) {
305
case "stars":
306
return "stars DESC, public_paths.last_edited DESC";
307
case "-stars":
308
return "stars ASC, public_paths.last_edited DESC";
309
case "views":
310
return "COALESCE(counter,0) DESC, public_paths.last_edited DESC";
311
case "-views":
312
return "COALESCE(counter,0) ASC, public_paths.last_edited DESC";
313
case "-last_edited":
314
return "public_paths.last_edited ASC";
315
default:
316
return "public_paths.last_edited DESC";
317
}
318
}
319
320